﻿/*
 * Cratos Server - Entity Access Layer (EAL)
 * Cratos EAL Base class
 * 
 * Copyright (c) 2011 Draco
 * 
 * This file is part of Cratos.
 * 
 * Cratos is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * Cratos is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Cratos.  If not, see <http://www.gnu.org/licenses/>.
 * 
 *       .==.        .==.
 *      //'^\\      //^'\\
 *     // ^ ^\(\__/)/^ ^^\\
 *    //^ ^^ ^/6  6\ ^^ ^ \\
 *   //^ ^^ ^/( .. )\^ ^ ^ \\
 *  // ^^ ^/\| V""V|/\^ ^ ^ \\
 * //^^/\/ /  '~~'  \ \/\^ ^ \\
 *  ------------------------------------
 *  HERE BE DRAGONS
 *  
 *  Johan Dercksen - johan@draco.co.za
 */
using System;
using System.Linq;
using System.Text;
using System.Data;
using System.Reflection;
using System.Configuration;
using System.Runtime.Remoting;
using System.Collections.Generic;
using System.Collections.ObjectModel;

using Microsoft.Practices.Unity;

using Cratos.Tools.General;
using Cratos.Tools.DataObject;

namespace Cratos.Server.Entity
{
	/// <summary>
	/// Cratos EAL Base class
	/// </summary>
	public abstract class CratosEALBase : IDisposable
	{
		#region Class Constant declarations
		#endregion

		#region Class Event declarations
		#endregion

		#region Class Variable declarations
		private bool										m_IsDisposed;
		private Collection<CratosError> m_Errors = new Collection<CratosError>();
		#endregion

		#region Class Constructors & Destructors
		/// <summary>
		/// Class Constructor
		/// </summary>
		public CratosEALBase() { }

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or
		/// resetting unmanaged resources.
		/// </summary>
		public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }

		/// <summary>
		/// Dispose method doing the disposing
		/// </summary>
		/// <param name="isDisposing"></param>
		protected virtual void Dispose(bool isDisposing)
		{
			// Declare the required variables

			// Check to see if Dispose has already been called.
			if (!this.m_IsDisposed)
			{
				// Dispose the objects if required
				if (isDisposing)
				{
					// Dispose of the required objects
					if (this.DBAccess != null) { this.DBAccess.Disconnect(); this.DBAccess = null; }
				}
			}

			// Set the disposed indicator
			this.m_IsDisposed = true;

			// Return to calling method
			return;
		}
		#endregion

		#region Class Properties
		/// <summary>
		/// Ignore Unity Indicator
		/// </summary>
		public bool IgnoreUnity { get; set; }

		/// <summary>
		/// Cratos Data Object
		/// </summary>
		protected IDOCratos DataObject { get; set; }

		/// <summary>
		/// Cratos Database Access object
		/// </summary>
		protected ICratosDataAccess	DBAccess { get; set; }

		/// <summary>
		/// Cratos Entity Error Collection
		/// </summary>
		public Collection<CratosError> Errors
		{
			get { return (this.m_Errors); }
		}
		#endregion

		#region Class Methods
		/// <summary>
		/// Setup the DB Connection
		/// </summary>
		/// <returns>A boolean indicating the success of the method</returns>
		protected virtual bool SetupDBConnection()
		{
			// Declare the required variables
			bool					returnValue = false;
			ObjectHandle	tempObject;

			try
			{
				// Make sure the DB Connection Name is valid
				if (ConfigurationManager.ConnectionStrings[this.DataObject.DBConnectionName] == null)
				{
					// Throw an exception
					throw new Exception("Connection Name not specified");
				}
				else
				{
					// Make sure we don't have a connection already
					if ((this.DBAccess == null) || (this.DBAccess.Connection.State != ConnectionState.Open))
					{
						// Handle the Unity indicator
						if (this.IgnoreUnity)
						{
							switch (this.DataObject.DBActionType)
							{
								case DOCratosActionType.StoredProcedure:
									// Create the relevant type
									tempObject		= Activator.CreateInstance("Cratos.Tools.DataAccess", "Cratos.Tools.DataAccess.CratosDBStoredProcedure");
									this.DBAccess = ((ICratosDataAccess) tempObject.Unwrap());
									break;

								case DOCratosActionType.SqlString:
									// Create the relevant type
									tempObject		= Activator.CreateInstance("Cratos.Tools.DataAccess", "Cratos.Tools.DataAccess.CratosDBSqlString");
									this.DBAccess = ((ICratosDataAccess) tempObject.Unwrap());
									break;

								default:
									throw new Exception("Invalid Database Command Type specified");
							}
						}
						else
						{
							switch (this.DataObject.DBActionType)
							{
								case DOCratosActionType.StoredProcedure:
									// Create the relevant type
									this.DBAccess = DOCratosGlobal.UnityContainer.Resolve<ICratosDataAccess>("CratosDBStoredProcedure");
									break;

								case DOCratosActionType.SqlString:
									// Create the relevant type
									this.DBAccess = DOCratosGlobal.UnityContainer.Resolve<ICratosDataAccess>("CratosDBSqlString");
									break;

								default:
									throw new Exception("Invalid Database Command Type specified");
							}
						}

						// Make sure we have a Data Access object
						if (this.DBAccess != null)
						{
							// Setup the connection
							this.DBAccess.ConnectionString = ConfigurationManager.ConnectionStrings[this.DataObject.DBConnectionName].ToString();

							// Connect to the database
							this.DBAccess.Connect();

							// Set the successful return value
							returnValue = true;
						}
						else { throw new Exception("Failed to create the required Cratos Data Access object"); }
					}
					else { returnValue = true; }
				}
			}
			catch (Exception runtimeException)
			{
				// Create and setup a new error
				this.Errors.Add(new CratosError("CratosEAL.SetupDBConnection", CratosLogSeverity.Exception,
																				"Runtime error ocurred", runtimeException));
			}

			// return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Add Parameter to DB Command
		/// </summary>
		/// <param name="parameterName">Parameter Name</param>
		/// <param name="dataType">Data Type</param>
		/// <param name="parameterValue">Parameter Value</param>
		/// <param name="isMandatory">Mandatory indicator</param>
		/// <param name="isIgnoreDefaultNull">Ignore default if null</param>
		/// <param name="useSqlDbType">Use SQL DB Type</param>
		/// <param name="sqlDbType">SQL Data type</param>
		protected void AddParameter(string parameterName, DbType dataType, object parameterValue,
																bool isMandatory, bool isIgnoreDefaultNull, bool useSqlDbType, SqlDbType sqlDbType)
		{
			// Declare the required variables
			bool isValidParameter = true;

			try
			{
				// Handle the data type
				switch (dataType)
				{
					case DbType.Byte:
						// Validate the Mandatory indicator
						if (isMandatory && !isIgnoreDefaultNull && (Convert.ToInt16(parameterValue) == 0)) { isValidParameter = false; }
						else if (!isMandatory && !isIgnoreDefaultNull && (Convert.ToInt16(parameterValue) == 0))
						{
							// Add the null parameter to the Command
							this.DBAccess.AddSqlParameter(parameterName, SqlDbType.TinyInt, null, ParameterDirection.Input);
						}
						else
						{
							// Add the parameter to the Command
							this.DBAccess.AddSqlParameter(parameterName, SqlDbType.TinyInt, parameterValue, ParameterDirection.Input);
						}

						break;

					case DbType.Int16:
					case DbType.Int32:
					case DbType.Int64:
						// Validate the Mandatory indicator
						if (isMandatory && !isIgnoreDefaultNull && (Convert.ToInt64(parameterValue) == 0)) { isValidParameter = false; }
						else if (!isMandatory && !isIgnoreDefaultNull && (Convert.ToInt64(parameterValue) == 0))
						{
							// Add the null parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, null);
						}
						else
						{
							// Add the parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, parameterValue);
						}

						break;

					case DbType.DateTime:
						// Validate the Mandatory indicator
						if (isMandatory && !isIgnoreDefaultNull && (Convert.ToDateTime(parameterValue).Year <= 1753)) { isValidParameter = false; }
						else if (!isMandatory && (Convert.ToDateTime(parameterValue).Year <= 1753))
						{
							// Add the null parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, null);
						}
						else
						{
							// Add the parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, parameterValue);
						}

						break;

					case DbType.Decimal:
						// Validate the Mandatory indicator
						if (isMandatory && !isIgnoreDefaultNull && (Convert.ToDecimal(parameterValue) == Decimal.Zero)) { isValidParameter = false; }
						else if (!isMandatory && (Convert.ToDecimal(parameterValue) == Decimal.Zero))
						{
							// Add the null parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, null);
						}
						else
						{
							// Handle the SQL Data Type if required
							if (useSqlDbType) { this.DBAccess.AddSqlParameter(parameterName, sqlDbType, parameterValue, ParameterDirection.Input); }
							else { this.DBAccess.AddParameter(parameterName, dataType, parameterValue); }
						}

						break;

					case DbType.String:
						// Validate the Mandatory indicator
						if (isMandatory && !isIgnoreDefaultNull && string.IsNullOrEmpty((string)parameterValue)) { isValidParameter = false; }
						else if (!isMandatory && string.IsNullOrEmpty((string)parameterValue))
						{
							// Add the null parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, null);
						}
						else
						{
							// Add the parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, parameterValue);
						}

						break;

					case DbType.Guid:
						// Validate the Mandatory indicator
						if (isMandatory && !isIgnoreDefaultNull && ((Guid)parameterValue == Guid.Empty)) { isValidParameter = false; }
						else if (!isMandatory && ((Guid)parameterValue == Guid.Empty))
						{
							// Add the null parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, null);
						}
						else
						{
							// Add the parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, parameterValue);
						}

						break;

					default:
						// Validate the Mandatory indicator
						if (isMandatory && (parameterValue == null)) { isValidParameter = false; }
						else
						{
							// Add the parameter to the Command
							this.DBAccess.AddParameter(parameterName, dataType, parameterValue);
						}

						break;
				}

				// Valid parameter
				if (!isValidParameter)
				{
					// Add an error to the error list
					throw new Exception(string.Format("DB Parameter [{0}] is mandatory but no value was supplied [{1}]",
																						parameterName, this.DataObject.GetType().ToString()));
				}
			}
			catch (Exception runtimeException)
			{
				// Create and setup a new error
				this.Errors.Add(new CratosError("CratosEAL.AddParameter", CratosLogSeverity.Exception,
																				"Runtime error ocurred", runtimeException));

			}

			// Return to calling method
			return;
		}

		/// <summary>
		/// Load Data from the Data Reader
		/// </summary>
		protected void LoadDataFromDataReader(IDOCratos cratosDO)
		{
			// Declare the required variables

			// Process all the Data Object columns
			foreach (DOCratosColumnAttribute currentColumn in cratosDO.AllDOColumns)
			{
				// Make sure the column exists in the data row
				if (this.DBAccess.ReaderContainsField(currentColumn.DBColumnName))
				{
					// Make sure the field contains a value
					if (this.DBAccess.ReaderIsFieldNull(currentColumn.DBColumnName))
					{
						// Set the property to its default value
						this.SetDOPropertyNull(cratosDO, currentColumn.Property, currentColumn.DbType);
					}
					else
					{
						// Set the property value
						this.SetDOPropertyValue(cratosDO, currentColumn.Property, currentColumn.DbType,
																		this.DBAccess.ReaderGetValue(currentColumn.DBColumnName, currentColumn.DbType));
					}
				}
			}

			// Set the loaded indicator
			cratosDO.IsLoaded = true;

			// Return to calling method
			return;
		}

		/// <summary>
		/// Set Data Object Property to Null
		/// </summary>
		/// <param name="doCratos">Cratos Data Object</param>
		/// <param name="propertyName"></param>
		/// <param name="dbType"></param>
		private void SetDOPropertyNull(IDOCratos doCratos, string propertyName, DbType dbType)
		{
			// Declare the required variables

			// Handle the relevant Data Type
			switch (dbType)
			{
				case DbType.Decimal:
					// Set the default null value
					doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, 0.0m, null);
					break;

				case DbType.Boolean:
					// Set the default null value
					doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, false, null);
					break;

				case DbType.DateTime:
					// Set the default null value
					doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, new DateTime(1753, 1, 1), null);
					break;

				case DbType.String:
					// Set the default null value
					doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, string.Empty, null);
					break;

				default:
					// Set the default null value
					doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, null, null);
					break;
			}

			// Return to calling method
			return;
		}

		/// <summary>
		/// Set Data Object Property Value
		/// </summary><param name="doCratos">Cratos Data Object</param>
		/// <param name="propertyName"></param>
		/// <param name="dbType"></param>
		/// <param name="newValue"></param>
		protected void SetDOPropertyValue(IDOCratos doCratos, string propertyName, DbType dbType, object newValue)
		{
			// Declare the required variables

			try
			{
				// Handle the relevant Data Type
				switch (dbType)
				{
					case DbType.Int16:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToInt16(newValue), null);
						break;

					case DbType.Int32:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToInt32(newValue), null);
						break;

					case DbType.Int64:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToInt64(newValue), null);
						break;

					case DbType.Decimal:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToDecimal(newValue), null);
						break;

					case DbType.DateTime:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToDateTime(newValue), null);
						break;

					case DbType.Boolean:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToBoolean(newValue), null);
						break;

					case DbType.String:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, Convert.ToString(newValue), null);
						break;

					case DbType.Guid:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, new Guid(Convert.ToString(newValue)), null);
						break;

					default:
						doCratos.GetType().GetProperty(propertyName).SetValue(doCratos, newValue, null);
						break;
				}
			}
			catch (Exception runtimeException)
			{
				// Add an error to the error collection
				this.Errors.Add(new CratosError(string.Format("{0}.{1}", runtimeException.Source, runtimeException.TargetSite),
																				CratosLogSeverity.Exception, 
																				string.Format("Runtime error occurred when setting {0}.{1}", doCratos.GetType(), propertyName),
																				runtimeException));
			}

			// Return to calling method
			return;
		}

		/// <summary>
		/// Process Embedded Object
		/// </summary>
		/// <param name="dataObject">Data Object definition</param>
		/// <param name="parentDOObject">Current Embedded Data Object</param>
		/// <param name="currentEmbeddedAttribute">Embedded Object Attribute</param>
		/// <returns>A boolean indicating the success of the method</returns>
		protected bool ProcessEmbeddedObject(IDOCratos dataObject, IDOCratos parentDOObject, 
																				 DOCratosEmbeddedAttribute currentEmbeddedAttribute)
		{
			// Declare the required variables
			bool														returnValue				= false;
			IDOCratos												embeddedDOObject	= null;
			DOCratosEmbeddedKeyAttribute[]	allKeyAttributes;
			PropertyInfo										embeddedDOProperty, parentProperty, embeddedProperty;

			try
			{
				// Retrieve the Embedded Data Object
				embeddedDOProperty = parentDOObject.GetType().GetProperty(currentEmbeddedAttribute.Property);
				embeddedDOObject   = (IDOCratos) dataObject.Clone();

				// Make sure we have data to work with
				if (embeddedDOObject == null) 
				{
					// Add an error to the Error Collection
					this.Errors.Add(new CratosError("CratosEALBase.ProcessEmbeddedObject", CratosLogSeverity.ValidationError,
																					string.Format("Embedded Object or Property not found [{0} - {1}]", 
																												parentDOObject.GetType(), currentEmbeddedAttribute.Name)));
				}
				else if (embeddedDOObject.DOAction != DOCratosAction.None)
				{
					#region Parameter Processing
					// Retrieve the Key Attributes
					allKeyAttributes = (DOCratosEmbeddedKeyAttribute[])embeddedDOProperty.GetCustomAttributes(typeof(DOCratosEmbeddedKeyAttribute), false);

					// Process all the Keys
					foreach (DOCratosEmbeddedKeyAttribute currentKey in allKeyAttributes)
					{
						// Retrieve the relevant property information
						embeddedProperty = embeddedDOObject.GetType().GetProperty(currentKey.EmbeddedKeyProperty);
						parentProperty   = parentDOObject.GetType().GetProperty(currentKey.ParentProperty);

						// Make sure both properties exist
						if ((embeddedProperty != null) && (parentProperty != null))
						{
							// Set the Embedded object key property values
							embeddedProperty.SetValue(embeddedDOObject, parentProperty.GetValue(parentDOObject, null), null);
						}
						else
						{
							// Add an error to the Error Collection
							this.Errors.Add(new CratosError("CratosEALBase.ProcessEmbeddedObject", CratosLogSeverity.ValidationError,
																							string.Format("Key Properties for embedded object {0} is invalid",
																														currentEmbeddedAttribute.Name)));
						}
					}
					#endregion

					// Perform the requested action
					if (embeddedDOObject.PerformAction()) { returnValue = true; }
					else
					{
						// Process any errors
						foreach (CratosError currentError in embeddedDOObject.Errors) { this.Errors.Add(currentError); }
					}

					// Update the Parent Data Object
					DOCratosHelper.SetPropertyValue(parentDOObject, currentEmbeddedAttribute.Property, embeddedDOObject);
				}
			}
			catch (Exception runtimeException)
			{
				// Create and setup a new error
				this.Errors.Add(new CratosError("CratosEALBase.ProcessEmbeddedObject", CratosLogSeverity.Exception,
																				"Runtime error ocurred", runtimeException));
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Process Embedded Collection
		/// </summary>
		/// <param name="doCollection">Data Object definition</param>
		/// <param name="parentDOObject">Parent Data Object</param>
		/// <param name="currentEmbeddedAttribute">Embedded Object Attribute</param>
		/// <returns>A boolean indicating the success of the method</returns>
		protected bool ProcessEmbeddedCollection(object doCollection, IDOCratos parentDOObject, 
																						 DOCratosEmbeddedAttribute currentEmbeddedAttribute)
		{
			// Declare the required variables
			bool														returnValue = false;
			object													sourceData;
			DOCratosEmbeddedKeyAttribute[]	embeddedKeyAttributes;

			try
			{
				// Retrieve the relevant object data
				embeddedKeyAttributes = (DOCratosEmbeddedKeyAttribute[]) parentDOObject.GetType().GetProperty(currentEmbeddedAttribute.Property).GetCustomAttributes(typeof(DOCratosEmbeddedKeyAttribute), false);

				// Perform the relevant action if required
				if (((DOCratosCollectionAction)DOCratosHelper.GetPropertyValue(doCollection, "CollectionAction")) != DOCratosCollectionAction.None)
				{
					#region Parameter / Filter Processing
					// Set the relevant Embedded Key data
					foreach (DOCratosEmbeddedKeyAttribute currentKey in embeddedKeyAttributes)
					{
						// Retrieve the relevant Source Data
						sourceData = parentDOObject.GetType().GetProperty(currentKey.ParentProperty).GetValue(parentDOObject, null);

						// Update the Key Property
						DOCratosHelper.SetPropertyValue(doCollection, currentKey.EmbeddedKeyProperty, sourceData);
					}
					#endregion

					// Send the request and handle any errors
					if (!((bool)DOCratosHelper.InvokeMethod(doCollection, "PerformAction", null)))
					{
						foreach (CratosError currentError in ((IEnumerable<object>)DOCratosHelper.GetPropertyValue(doCollection, "Errors")))
						{
							this.Errors.Add(currentError);
						}
					}
				}
			}
			catch (Exception runtimeException)
			{
				// Create and setup a new error
				this.Errors.Add(new CratosError("CratosEALBase.ProcessEmbeddedCollection", CratosLogSeverity.Exception,
																				"Runtime error ocurred", runtimeException));
			}

			// Return to calling method
			return(returnValue);
		}
		#endregion

		#region Class Support Methods
		#endregion
	}
}
